__all__ = ['parse_command_line', 'log_config', 'init', 'schedule_tasks', 'clean']


import argparse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import logging
import os
import sys
import types

import util


__author__  = 'ZL <837806295@qq.com>'
__product__ = 'keepitsecret'
__version__ = '2.0.10'
__date__    = '2023/04/08'


log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())  # 如果宿主未配置日志处理器，则丢弃日志

args = types.SimpleNamespace()

password = None

cipher_key = None
cipher_iv = b'0123456789abcdef'
cipher_extension = '.kis'


class Secret:
    """封装AES算法

    AES算法的密钥长度需要是16、24或32个字节，会根据密钥长度选择相应的AES-128、AES-192或
    AES-256算法，所用的模式是CBC，并约定加、解密两边都使用“0123456789abcdef”这16字节作为
    固定的IV值，AES算法待处理数据长度需要是16字节或其整数倍，如果密钥长度、IV长度和待处理数据
    长度不满足要求，AES算法会产生异常

    如果密钥不够指定长度，会使用PKCS7算法扩充到16、24或32中最接近的字节数，如果密钥长度超过
    32字节会截断，然后提供给AES算法

    待加密的数据会用PKCS7算法以16字节为目标块长度进行填充，如果原数据长度不是16整数倍，会向
    上扩充到最接近的整数，如果是16整数倍，则会附加16个内容整数值是16的字节，这样是为了方便恢
    复，只用提供最后一个解密后的块就能恢复到PKCS7算法填充前的样子

    PKCS7填充算法，如果待扩充数据的长度是目标长度块的整数倍，会在最后面附加一个完整的目
    标长度块，注意空串也是附加完整的块，填充的每个字节的内容的整数值都等于目标长度，如果待扩
    充数据的长度不是目标长度块的整数倍，就是最后一个块被扩充到目标长度块，以缺失的字节数作为
    填充字节的内容，比如6个字节扩充到16字节，则会填充10个整数值等于10的字节，这样填充后只
    用提供最后一个块，根据最后一个块最后一个字节的内容，就知道最后一个块要移除字节数，如果
    最后一个块最后一个字节内容的整数值是16，那最后一整个块都要移除

    一个实例只能单独用于加密或者解密，不能同时用来加密和解密，在同一个实例上调用加密接口后再
    调用解密接口会产生异常，反过来也是一样

    一个实例的多次加密调用是关联的，一个实例的的多次解密调用也是关联的，所以一个实例只能用于
    对一份数据一次完整的加密或者解密，当需要处理很长数据时，可以分多次调用加密或者解密接口，
    前面的调用给的数据要是16字节的整数倍，加密时最后一次调用不管数据长度是不是16字节的整数倍，
    都要对数据进行填充，解密时最后一次调用得到数据要进行解填充
    """

    def __init__(self, key, iv_=cipher_iv):
        if not isinstance(key, bytes):
            log.critical('Secret | init ... failed ... exit | illegal input')
            sys.exit(1)

        key = self.normalize_key(key)

        try:
            self.generator = AES.new(key, AES.MODE_CBC, iv_)
        except Exception:
            log.exception('Secret | init ... exception ... exit')
            sys.exit(1)

    def encrypt(self, data):
        if not isinstance(data, bytes):
            log.critical('Secret | encrypt ... failed ... exit | illegal input')
            sys.exit(1)

        if not data:
            log.warning('Secret | encrypt ... ongoing | empty')
            return b''

        try:
            data = self.generator.encrypt(data)
        except Exception:
            log.exception('Secret | encrypt ... exception ... exit')
            sys.exit(1)

        log.debug('Secret | encrypt ... successed')
        return data

    def decrypt(self, data):
        if not isinstance(data, bytes) or not data:
            log.critical('Secret | decrypt ... failed ... exit | illegal input')
            sys.exit(1)

        if len(data) % 16:
            log.critical('Secret | decrypt ... failed ... exit | invalid input')
            sys.exit(1)

        try:
            data = self.generator.decrypt(data)
        except Exception:
            log.exception('Secret | decrypt ... exception ... exit')
            sys.exit(1)

        log.debug('Secret | decrypt ... successed')
        return data

    def pad(self, data):
        if not isinstance(data, bytes):
            log.critical('Secret | pad ... failed ... exit | illegal input')
            sys.exit(1)

        log.info('Secret | pad ... ongoing | length: %d', len(data))
        data = pad(data, 16, 'pkcs7')

        return data

    def unpad(self, data):
        if not isinstance(data, bytes) or not data:
            log.critical('Secret | unpad ... failed ... exit | illegal input')
            sys.exit(1)

        if len(data) % 16:
            log.critical('Secret | unpad ... failed ... exit | invalid input')
            sys.exit(1)

        log.info('Secret | unpad ... ongoing | last 16 bytes: %s',
                 data[len(data) - 16 :])

        try:
            data = unpad(data, 16, 'pkcs7')
        except Exception:
            log.exception('Secret | unpad ... exception ... exit')
            sys.exit(1)

        return data

    @staticmethod
    def normalize_key(key):
        if not isinstance(key, bytes):
            log.critical('Secret | normalize key ... failed ... exit | illegal '
                         'input')
            sys.exit(1)

        if not key:
            log.warning('Secret | normalize key ... ongoing | empty')

        length = len(key)
        if length < 16:
            key = pad(key, 16, 'pkcs7')
        elif length > 16 and length < 24:
            key = pad(key, 24, 'pkcs7')
        elif length > 24 and length < 32:
            key = pad(key, 32, 'pkcs7')
        elif length > 32:
            key = key[:32]
            log.warning('Secret | normalize key ... ongoing | more than 32 '
                        'characters')

        log.debug('Secret | normalize key ... successed')
        return key

    @staticmethod
    def check_padding(data, key):
        if (not isinstance(data, bytes)
                or (len(data) != 16 and len(data) != 32)
                or not isinstance(key, bytes)):
            log.critical('Secret | check padding ... failed ... exit | illegal '
                         'input')
            sys.exit(1)

        if len(data) == 16:
            s = Secret(key)
            data = s.decrypt(data)
        else:
            s = Secret(key, data[:16])
            data = s.decrypt(data[16:])

        log.debug('Secret | check padding ... ongoing | data: %s', data)

        try:
            unpad(data, 16, 'pkcs7')
        except Exception:
            log.exception('Secret | check padding ... exception | unpad')
            return False

        return True

def is_supported_key(key):
    """是否是可支持的密钥字符

    AES算法对密钥是没有限制的，不过为了方便输入密钥，这里只允许可打印字符作为密钥字符，一共
    是95个字符：0-9a-zA-Z `~!@#$%^&*()-_=+[{}]\|;:'",<.>/?
    """

    if not isinstance(key, str) or not key:
        log.error('is supported key ... no | illegal input')
        return False

    if not util.is_printable_ascii(key):
        log.error('is supported key ... no')
        return False

    log.debug('is support key ... yes')
    return True

def get_key(key_sha512):
    if (key_sha512 is not None
            and not isinstance(key_sha512, str)):
        log.critical('get key ... failed ... exit | illegal input')
        sys.exit(1)

    if key_sha512 is None:
        log.info('get key ... ongoing | no check')
        return util.input_double('secret password', 3, is_supported_key)
    else:
        log.info('get key ... ongoing | check')
        return util.input_with_sha512('secret password', 3, is_supported_key,
                                      key_sha512)

def is_encrypted_path(path):
    """看路径是否是加密文件路径

    假设给的路径合法，看给的路径是不是加密文件路径，只看扩展名
    """

    if not isinstance(path, str) or not path:
        log.error('is encrypted path ... no | illegal input')
        return False

    try:
        _, filename = os.path.split(path)
    except Exception:
        log.exception('is encrypted path ... exception ... exit')
        sys.exit(1)

    if (not util.has_extension(path, cipher_extension)
            or filename == cipher_extension):
        log.debug('is encrypted path ... no | extension')
        return False

    log.debug('is encrypted path ... yes')
    return True

def generate_encrypted_path(path):
    """生成加密文件路径

    为指定的文件生成其加密文件路径，这里生成方法就是在原文件路径上附加一个扩展名
    """

    if not isinstance(path, str) or not path:
        log.error('generate encrypted path ... failed | illegal input')
        return ''

    if util.has_extension(path, cipher_extension):
        log.warning(
            'generate encrypted path ... ongoing | path: %s, extension: %s',
            path, cipher_extension)

    log.debug('generate encrypted path ... successed')
    return path + cipher_extension

def generate_decrypted_path(path):
    """生成解密文件路径

    为指定的文件生成其解密文件路径，这里生成方法就是在原文件路径上移除末尾扩展名，给定的路径
    要是加密文件的路径，否则返回空串
    """

    if not isinstance(path, str) or not path:
        log.error('generate decrypted path ... failed | illegal input')
        return ''

    if not is_encrypted_path(path):
        log.error('generate decrypted path ... failed | extension')
        return ''

    log.debug('generate decrypted path ... successed')
    return path[: len(path) - len(cipher_extension)]

def check_padding(path, key):
    """检查文件是不是能解填充

    会解密文件最后16字节，然后解填充，不出异常就是能解填充
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not is_supported_key(key)):
        log.error('check padding ... error | illegal input')
        return False

    file_size = os.path.getsize(path)
    if file_size <= 0 or file_size % 16:
        log.warning('check padding ... error | invalid input')
        return False

    ok = True

    try:
        with open(path, 'rb') as f:
            if file_size < 32:
                buffer = f.read(16)
                ok = Secret.check_padding(buffer, key.encode('utf-8'))
            else:
                if f.seekable():
                    f.seek(-32, os.SEEK_END)
                    buffer = f.read(32)
                    ok = Secret.check_padding(buffer, key.encode('utf-8'))
                else:
                    log.warning('check padding ... ongoing | not seekable')
    except Exception:
        log.exception('check padding ... exception')
        return False

    if ok:
        log.debug('check padding ... ok')
    else:
        log.debug('check padding ... error')

    return ok

def is_encrypted_file(path, check=False, key=''):
    """筛查是不是加密文件

    不开启检查的话，扩展名符合要求，文件大小符合要求，就认为是加密文件

    开启检查的话，还会解密文件最后16字节，如果能正常解填充，才认为是加密文件
    """

    if not isinstance(path, str) or not path:
        log.error('is encrypted file ... no | illegal input')
        return False

    if not os.path.isfile(path):
        log.debug('is encrypted file .... no | not file: %s', path)
        return False

    if not is_encrypted_path(path):
        log.debug('is encrypted file .... no | extension')
        return False

    file_size = os.path.getsize(path)
    if file_size <= 0:
        log.warning('is encrypted file ... no | empty')
        return False

    if file_size % 16:
        log.warning('is encrypted file ... no | file size')
        return False

    if check and not check_padding(path, key):
        log.warning('is encrypted file ... no | padding')
        return False

    log.debug('is encrypted file ... yes')
    return True

def is_decrypted_file(path):
    """不查看文件内容，筛查是不是解密文件

    用路径生成加密文件路径，生成结果被判断是加密文件，则认为是解密文件
    """

    if not isinstance(path, str) or not path:
        log.error('is decrypted file ... no | illegal input')
        return False

    if not os.path.isfile(path):
        log.debug('is decrypted file ... no | not file: %s', path)
        return False

    if os.path.getsize(path) <= 0:
        log.warning('is decrypted file ... no | empty')
        return False

    encrypted_path = generate_encrypted_path(path)
    if not is_encrypted_file(encrypted_path):
        log.debug('is decrypted file ... no')
        return False

    log.debug('is decrypted file ... yes')
    return True

def choose_files_to_encrypt(root,
                            files,
                            ignore_hide,
                            ignore_encrypted,
                            include_extension,
                            extension,
                            exclude_pattern,
                            pattern):
    """选择需要被加密的文件

    扩展名与搜索字符串两种条件同时存在时，要同时满足才认为是需要加密的文件
    """

    if (not isinstance(root, str)
            or not root
            or not isinstance(files, list)):
        log.error('choose files to encrypt ... failed | illegal input')
        return []

    if not files:
        log.warning('choose files to encrypt ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('choose files to encrypt ... ongoing | ignore: %s', path)
            continue

        if is_encrypted_file(path) and ignore_encrypted:
            log.info('choose files to encrypt ... ongoing | ignore: %s', path)
            continue

        yes = util.is_requested_file_by_extension(root,
                                                  filename,
                                                  include_extension,
                                                  extension)
        yes2 = util.is_requested_file_by_pattern(root,
                                                 filename,
                                                 exclude_pattern,
                                                 pattern)
        if yes and yes2:
            requested.append(path)
            log.debug('choose files to encrypt ... ongoing | hit: %s', path)
        else:
            log.debug('choose files to encrypt ... ongoing | not hit: %s', path)

    log.debug('choose files to encrypt ... successed')
    return requested

def choose_files_to_decrypt(root,
                            files,
                            ignore_hide,
                            exclude_pattern,
                            pattern):
    """选择需要被解密的文件

    只考虑具有加密扩展名的文件，其它的筛选条件都是在这个基础之上进行的
    """

    if (not isinstance(root, str)
            or not root
            or not isinstance(files, list)):
        log.error('choose files to decrypt ... failed | illegal input')
        return []

    if not files:
        log.warning('choose files to decrypt ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('choose files to decrypt ... ongoing | ignore: %s', path)
            continue

        if not is_encrypted_file(path):
            log.debug('choose files to decrypt ... ongoing | ignore: %s', path)
            continue

        yes = util.is_requested_file_by_pattern(root,
                                                filename,
                                                exclude_pattern,
                                                pattern)
        if yes:
            requested.append(path)
            log.debug('choose files to decrypt ... ongoing | hit: %s', path)
        else:
            log.debug('choose files to decrypt ... ongoing | not hit: %s', path)

    log.debug('choose files to decrypt ... successed')
    return requested

def choose_requested_file(root, files, ignore_hide, encrypted, decrypted):
    """选择需要的文件
    """

    if not isinstance(root, str) or not root:
        log.error('choose requested file ... failed | illegal input')
        return []

    if not isinstance(files, list) or not files:
        log.warning('choose requested file ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('choose requested file ... ongoing | ignore: %s', path)
            continue

        if encrypted and not decrypted:
            if is_encrypted_file(path):
                requested.append(path)
                log.debug('choose requested file ... ongoing | hit: %s', path)
            else:
                log.debug('choose requested file ... ongoing | not hit: %s',
                          path)
        elif decrypted and not encrypted:
            if is_decrypted_file(path):
                requested.append(path)
                log.debug('choose requested file ... ongoing | hit: %s', path)
            else:
                log.debug('choose requested file ... ongoing | not hit: %s',
                          path)
        elif encrypted and decrypted:
            if is_encrypted_file(path) and is_decrypted_file(path):
                requested.append(path)
                log.debug('choose requested file ... ongoing | hit: %s', path)
            else:
                log.debug('choose requested file ... ongoing | not hit: %s',
                          path)
        else:
            log.critical('choose requested file ... failed ... exit | not '
                         'supported')
            sys.exit(1)

    log.debug('choose requested file ... successed')
    return requested

def count_requested_file(root, files, ignore_hide, encrypted, decrypted):
    """统计需要的文件的数量
    """

    if not isinstance(root, str) or not root:
        log.error('count requested file ... failed | illegal input')
        return 0

    if not isinstance(files, list) or not files:
        log.warning('count requested file ... ignored | invalid input')
        return 0

    requested = 0
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('count requested file ... ongoing | ignore: %s', path)
            continue

        if encrypted and not decrypted:
            if is_encrypted_file(path):
                requested += 1
                log.debug('count requested file ... ongoing | hit: %s', path)
            else:
                log.debug('count requested file ... ongoing | not hit: %s',
                          path)
        elif decrypted and not encrypted:
            if is_decrypted_file(path):
                requested += 1
                log.debug('count requested file ... ongoing | hit: %s', path)
            else:
                log.debug('count requested file ... ongoing | not hit: %s',
                          path)
        elif encrypted and decrypted:
            if is_encrypted_file(path) and is_decrypted_file(path):
                requested += 1
                log.debug('count requested file ... ongoing | hit: %s', path)
            else:
                log.debug('count requested file ... ongoing | not hit: %s',
                          path)
        else:
            log.critical('count requested file ... failed ... exit | not '
                         'supported')
            sys.exit(1)

    log.debug('count requested file ... successed')
    return requested

def choose_special_file(root, files, ignore_hide):
    """选取具有加密扩展名但不是加密文件的特殊文件
    """

    if not isinstance(root, str) or not root:
        log.error('choose special file ... failed | illegal input')
        return []

    if not isinstance(files, list) or not files:
        log.warning('choose special file ... ignored | invalid input')
        return []

    requested = []
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('choose special file ... ongoing | ignore: %s', path)
            continue

        if (util.has_extension(filename, cipher_extension)
                and not is_encrypted_file(path)):
            requested.append(path)
            log.debug('choose special file ... ongoing | hit: %s', path)
        else:
            log.debug('choose special file ... ongoing | not hit: %s', path)

    log.debug('choose special file ... successed')
    return requested

def count_special_file(root, files, ignore_hide):
    """统计具有加密扩展名但不是加密文件的特殊文件数量
    """

    if not isinstance(root, str) or not root:
        log.error('count special file ... failed | illegal input')
        return 0

    if not isinstance(files, list) or not files:
        log.warning('count special file ... ignored | invalid input')
        return 0

    requested = 0
    for filename in files:
        path = os.path.join(root, filename)
        if util.is_hide(filename) and ignore_hide:
            log.info('count special file ... ongoing | ignore: %s', path)
            continue

        if (util.has_extension(filename, cipher_extension)
                and not is_encrypted_file(path)):
            requested += 1
            log.debug('count special file ... ongoing | hit: %s', path)
        else:
            log.debug('count special file ... ongoing | not hit: %s', path)

    log.debug('count special file ... successed')
    return requested

def encrypt_single_file(path, key, force, bytes_per_read, discard):
    """使用AES算法加密单个文件

    Args:
        path(str): 待加密的文件的绝对路径
        key(bytes): AES算法所需密钥
        force(bool): 当用来存储加密内容的文件的绝对路径已存在时，是替换，还是跳过
        bytes_per_read(int): 每次读取文件多少字节来操作
        discard(bool): 生成加密文件成功后，是否删除待加密文件

    Returns:
        成功返回True，跳过返回None，失败返回False

        有两种情况会跳过，一种是空文件时，另一种是存储加密内容的文件的绝对路径已存在且决定不
        替换时

    Return type: bool or None

    Raises:
        SystemExit
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not is_supported_key(key)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or bytes_per_read % 16):
        log.error('encrypt single file ... failed | illegal input')
        return False

    if os.path.getsize(path) <= 0:
        log.warning('encrypt single file ... ignored | empty')
        return None

    encrypted_path = generate_encrypted_path(path)
    if os.path.exists(encrypted_path):
        if force:
            if not os.path.isfile(encrypted_path):
                log.error('encrypt single file ... failed | not file: %s',
                          encrypted_path)
                return False
        else:
            log.warning('encrypt single file ... ignored | exist: %s',
                        encrypted_path)
            return None

    try:
        with open(path, 'rb') as src, open(encrypted_path, 'wb') as dest:
            s = Secret(key.encode('utf-8'))

            last_buffer = b''
            while True:
                buffer = src.read(bytes_per_read)
                if buffer:
                    if last_buffer:
                        dest.write(s.encrypt(last_buffer))

                    last_buffer = buffer
                else:
                    if last_buffer:
                        # 没数据了，所以上一次读到的是文件最后部分数据，要填充
                        dest.write(s.encrypt(s.pad(last_buffer)))

                    break
    except Exception:
        log.exception('encrypt single file ... exception')
        return False

    if discard:
        try:
            os.remove(path)
            log.info('encrypt single file ... ongoing | remove')
        except Exception:
            log.exception('encrypt single file ... exception | remove')
            return False

    log.info('encrypt single file ... successed')
    return True

def encrypt_file():
    log.info('encrypt file ... ongoing')

    tristate = encrypt_single_file(args.path,
                                   cipher_key,
                                   args.force,
                                   args.mb_per_read * 1024 * 1024,
                                   args.discard)
    if tristate is None:
        util.prompt('Ignore')
    else:
        util.prompt_assert(tristate, 'Encrypt')

    log.info('encrypt file ... done')

def encrypt_single_file_in_sandbox(path, key, force, bytes_per_read, discard):
    """使用AES算法加密单个文件

    使用AES算法加密单个文件，会拦截导致的任何异常，防止直接退出，以免影响别的任务
    """

    log.info('encrypt single file in sandbox ... ongoing | path: %s, '
             'force: %s, bytes_per_read: %d, discard: %s',
             path,
             force,
             bytes_per_read,
             discard)

    try:
        tristate = encrypt_single_file(path, key, force, bytes_per_read,
                                       discard)
    except Exception:
        log.exception('encrypt single file in sandbox ... exception | '
                      'exception')
        return False
    except SystemExit as e:
        log.exception(
            'encrypt single file in sandbox ... exception | systemexit, '
            'exit code: %d',
            e.code)
        return False

    log.info('encrypt single file in sandbox ... done')
    return tristate

def encrypt_for_dir():
    log.info('encrypt for dir ... ongoing')

    def on_error(e):
        log.critical('encrypt for dir ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 选择需要的文件，然后为其加密，统计执行结果
        requested = choose_files_to_encrypt(root,
                                            files,
                                            not args.expose,
                                            not args.notice_encrypted,
                                            args.include_extension,
                                            args.extension,
                                            args.exclude_pattern,
                                            args.pattern)
        for path in requested:
            util.prompt_status('Encrypt %s' % path, 'ongoing')
            tristate = encrypt_single_file_in_sandbox(
                path,
                cipher_key,
                args.force,
                args.mb_per_read * 1024 * 1024,
                args.discard)
            util.prompt_tristate(tristate, 'Encrypt %s' % path, False)
            util.make_statistics(path, tristate, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('encrypt for dir ... done')

def encrypt():
    log.info('encrypt ... ongoing')

    if os.path.isfile(args.path):
        encrypt_file()
    elif os.path.isdir(args.path):
        encrypt_for_dir()

    log.info('encrypt ... done')

def decrypt_single_file(path, key, force, bytes_per_read, discard):
    """使用AES算法解密单个文件

    Args:
        path(str): 待解密的文件的绝对路径
        key(bytes): AES算法所需密钥
        force(bool): 当用来存储解密内容的文件的绝对路径已存在时，是替换，还是跳过
        bytes_per_read(int): 每次读取文件多少字节来操作
        discard(bool): 生成解密文件成功后，是否删除待解密文件

    Returns:
        成功返回True，跳过返回None，失败返回False

        有两种情况会跳过，一种是空文件时，另一种是存储解密内容的文件的绝对路径已存在且决定不
        替换时

    Return type: bool or None

    Raises:
        SystemExit
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not is_supported_key(key)
            or not isinstance(bytes_per_read, int)
            or bytes_per_read <= 0
            or bytes_per_read % 16):
        log.error('decrypt single file ... failed | illegal input')
        return False

    if os.path.getsize(path) <= 0:
        log.warning('decrypt single file ... ignored | empty')
        return None

    if not is_encrypted_file(path, True, key):
        log.error('decrypt single file ... failed | invalid input')
        return False

    decrypted_path = generate_decrypted_path(path)
    if os.path.exists(decrypted_path):
        if force:
            if not os.path.isfile(decrypted_path):
                log.error('decrypt single file ... failed | not file: %s',
                          decrypted_path)
                return False
        else:
            log.warning('decrypt single file ... ignored | exist: %s',
                        decrypted_path)
            return None

    try:
        with open(path, 'rb') as src, open(decrypted_path, 'wb') as dest:
            s = Secret(key.encode('utf-8'))

            last_buffer = b''
            while True:
                buffer = src.read(bytes_per_read)
                if buffer:
                    if last_buffer:
                        dest.write(s.decrypt(last_buffer))

                    last_buffer = buffer
                else:
                    if last_buffer:
                        # 没数据了，所以上一次读到的是文件最后部分数据，要解填充
                        dest.write(s.unpad(s.decrypt(last_buffer)))

                    break
    except Exception:
        log.exception('decrypt single file ... exception')
        return False

    if discard:
        try:
            os.remove(path)
            log.info('decrypt single file ... ongoing | remove')
        except Exception:
            log.exception('decrypt single file ... exception | remove')
            return False

    log.info('decrypt single file ... successed')
    return True

def decrypt_file():
    log.info('decrypt file ... ongoing')

    tristate = decrypt_single_file(args.path,
                                   cipher_key,
                                   args.force,
                                   args.mb_per_read * 1024 * 1024,
                                   args.discard)
    if tristate is None:
        util.prompt('Ignore')
    else:
        util.prompt_assert(tristate, 'Decrypt')

    log.info('decrypt file ... done')

def decrypt_single_file_in_sandbox(path, key, force, bytes_per_read, discard):
    """使用AES算法解密单个文件

    使用AES算法解密单个文件，会拦截导致的任何异常，防止直接退出，以免影响别的任务
    """

    log.info('decrypt single file in sandbox ... ongoing | path: %s, '
             'force: %s, bytes_per_read: %d, discard: %s',
             path,
             force,
             bytes_per_read,
             discard)

    try:
        tristate = decrypt_single_file(path, key, force, bytes_per_read,
                                       discard)
    except Exception:
        log.exception('decrypt single file in sandbox ... exception | '
                      'exception')
        return False
    except SystemExit as e:
        log.exception(
            'decrypt single file in sandbox ... exception | systemexit, '
            'exit code: %d',
            e.code)
        return False

    log.info('decrypt single file in sandbox ... done')
    return tristate

def decrypt_for_dir():
    log.info('decrypt for dir ... ongoing')

    def on_error(e):
        log.critical('decrypt for dir ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.path, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 选择需要的文件，然后为其解密，统计执行结果
        requested = choose_files_to_decrypt(root,
                                            files,
                                            not args.expose,
                                            args.exclude_pattern,
                                            args.pattern)
        for path in requested:
            util.prompt_status('Decrypt %s' % path, 'ongoing')
            tristate = decrypt_single_file_in_sandbox(
                path,
                cipher_key,
                args.force,
                args.mb_per_read * 1024 * 1024,
                args.discard)
            util.prompt_tristate(tristate, 'Decrypt %s' % path, False)
            util.make_statistics(path, tristate, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('decrypt for dir ... done')

def decrypt():
    log.info('decrypt ... ongoing')

    if os.path.isfile(args.path):
        decrypt_file()
    elif os.path.isdir(args.path):
        decrypt_for_dir()

    log.info('decrypt ... done')

def list_special():
    log.info('list special ... ongoing')

    def on_error(e):
        log.critical('list special ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.dir, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = count_special_file(root, files, not args.expose)
            total += requested
        else:
            requested = choose_special_file(root, files, not args.expose)
            total += requested

    util.prompt_batch2(total)

    log.info('list special ... done')

def list_empty():
    log.info('list empty ... ongoing')

    def on_error(e):
        log.critical('list empty ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.dir, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = util.count_empty_file(root, files, not args.expose)
            total += requested
        else:
            requested = util.choose_empty_file(root, files, not args.expose)
            total += requested

    util.prompt_batch2(total)

    log.info('list empty ... done')

def list_by_extension():
    log.info('list by extension ... ongoing')

    def on_error(e):
        log.critical('list by extension ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.dir, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = util.count_file_by_extension(root,
                                                     files,
                                                     not args.expose,
                                                     cipher_extension)
            total += requested
        else:
            requested = util.choose_file_by_extension(root,
                                                      files,
                                                      not args.expose,
                                                      cipher_extension)
            total += requested

    util.prompt_batch2(total)

    log.info('list by extension ... done')

def list_requested(encrypted, decrypted):
    log.info('list requested ... ongoing')

    def on_error(e):
        log.critical('list requested ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = 0 if args.count else []
    for root, dirs, files in os.walk(args.dir, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        if args.count:
            requested = count_requested_file(root, files, not args.expose,
                                             encrypted, decrypted)
            total += requested
        else:
            requested = choose_requested_file(root, files, not args.expose,
                                              encrypted, decrypted)
            total += requested

    util.prompt_batch2(total)

    log.info('list requested ... done')

def list_for_dir():
    log.info('list for dir ... ongoing')

    if args.range == 'encrypted':
        list_requested(True, False)
    elif args.range == 'decrypted':
        list_requested(False, True)
    elif args.range == 'both':
        list_requested(True, True)
    elif args.range == 'extension':
        list_by_extension()
    elif args.range == 'special':
        list_special()
    elif args.range == 'empty':
        list_empty()

    log.info('list for dir ... done')

def list_():
    log.info('list ... ongoing')

    list_for_dir()

    log.info('list ... done')

def remove_by_extension():
    log.info('remove by extension ... ongoing')

    def on_error(e):
        log.critical('remove by extension ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.dir, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        # 按扩展名选择文件，然后为其移除摘要，统计执行结果
        requested = util.choose_file_by_extension(root,
                                                  files,
                                                  not args.expose,
                                                  cipher_extension)
        for path in requested:
            ok = util.remove_file_in_sandbox(path)
            util.prompt_assert(ok, 'Remove %s' % path, False)
            util.make_statistics(path, ok, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('remove by extension ... done')

def remove_requested(encrypted, decrypted):
    log.info('remove requested ... ongoing')

    def on_error(e):
        log.critical('remove requested ... failed | scan %s', e.filename)
        util.prompt_assert(False, 'Scan %s' % e.filename)

    total = [0]
    ignored = []
    failed = []
    for root, dirs, files in os.walk(args.dir, True, on_error):
        if not args.expose:
            util.strip_hide_dir(root, dirs)  # 剥除以“.”开头目录，之后便不用再遍历

        requested = choose_requested_file(root, files, not args.expose,
                                          encrypted, decrypted)
        for path in requested:
            ok = util.remove_file_in_sandbox(path)
            util.prompt_assert(ok, 'Remove %s' % path, False)
            util.make_statistics(path, ok, total, ignored, failed)

    util.prompt_batch(total[0], ignored, failed)

    log.info('remove requested ... done')

def remove_for_dir():
    log.info('remove for dir ... ongoing')

    if args.range == 'encrypted':
        remove_requested(True, False)
    elif args.range == 'decrypted':
        remove_requested(False, True)
    elif args.range == 'both':
        remove_requested(True, True)
    elif args.range == 'extension':
        remove_by_extension()

    log.info('remove for dir ... done')

def remove():
    log.info('remove ... ongoing')

    remove_for_dir()

    log.info('remove ... done')

def check_args(args, subcommand):
    if args.subcommand == 'encrypt':
        if args.include_extension and not args.extension:
            subcommand.error('arguement -i/--include_extension: should be used '
                             'with arguement -t/--extension')

    if args.subcommand in ['encrypt', 'decrypt']:
        if args.exclude_pattern and not args.pattern:
            subcommand.error('arguement -x/--exclude_pattern: should be used '
                             'with arguement -p/--pattern')

    return args

def parse_command_line():
    parser = argparse.ArgumentParser(
        prog=__product__,
        description='加、解密指定的文件或者指定的目录下的文件')

    parser.add_argument('-v', '--version',
                        action='version',
                        version='%(prog)s ' + __version__)

    subparsers = parser.add_subparsers(
        title='subcommands',
        required=True,
        dest='subcommand',
        description='子命令集，由相应的子命令完成相应的功能')

    common = argparse.ArgumentParser(add_help=False)
    common.add_argument('-k', '--key',
                        type=util.arg_type_sha512,
                        help='加解密需要密码，如果这个选项给出密码的sha512串，则会使用'
                             '其来校验密码输入是否正确，不使用这个选项时只是输入两次做前'
                             '后对比校验')
    common.add_argument('-m', '--mb_per_read',
                        type=util.arg_type_mb_per_read,
                        default=4,
                        help='每次操作读文件数据的大小，单位是M，最小为1，最大为1024，'
                             '默认是4')
    common.add_argument('-f', '--force',
                        action='store_true',
                        help='在加解密过程中，需要生成一些文件，如果这些文件所用的路径存'
                             '在时，是否强制覆盖，默认跳过，有此选项则覆盖')
    common.add_argument('-d', '--discard',
                        action='store_true',
                        help='加解密文件生成后是否删除原来的文件')
    common.add_argument('-e', '--expose',
                        action='store_true',
                        help='加解密目录下文件时，默认会忽略目录下所有以“.”字符开头的文'
                             '件或者目录，有此选项则不忽略')

    # encrypt子命令，用于加密
    encrypt = subparsers.add_parser(
        'encrypt',
        help='加密指定文件或者指定目录下的文件',
        parents=[util.arg_log(), util.arg_shutdown(), common])
    encrypt.add_argument('path',
                         type=util.arg_type_path,
                         help='待加密的文件完整路径或者待加密的文件所在目录的完整路径')
    encrypt.add_argument('-n', '--notice_encrypted',
                         action='store_true',
                         help='加密目录下文件时，默认会忽略目录下所有以加密扩展名结尾的'
                              '文件，有此选项则不忽略')
    encrypt.add_argument('-t', '--extension',
                         type=util.arg_type_non_empty2,
                         nargs='+',
                         default=None,
                         help='加密目录下文件时，默认会加密所有符合的文件，有此选项则忽'
                              '略有指定扩展名的文件')
    encrypt.add_argument('-i', '--include_extension',
                         action='store_true',
                         help='此选项需与extension选项一起使用，加密目录下文件时，默认'
                              '会加密所有符合的文件，有此选项则只加密有指定扩展名的文'
                              '件，由extension选项指定扩展名')
    encrypt.add_argument('-p', '--pattern',
                         type=util.arg_type_non_empty2,
                         nargs='+',
                         default=None,
                         help='加密目录下文件时，默认会加密所有符合的文件，有此选项则只'
                              '加密文件名中含有指定内容的文件')
    encrypt.add_argument('-x', '--exclude_pattern',
                         action='store_true',
                         help='此选项需与pattern选项一起使用，加密目录下文件时，默认'
                              '会加密所有符合的文件，有此选项则只加密文件名中不含指定'
                              '内容的文件')

    # decrypt子命令，用于解密
    decrypt = subparsers.add_parser(
        'decrypt',
        help='解密指定文件或者指定目录下的文件',
        parents=[util.arg_log(), util.arg_shutdown(), common])
    decrypt.add_argument('path',
                         type=util.arg_type_path,
                         help='待解密的文件完整路径或者待解密的文件所在目录的完整路径')
    decrypt.add_argument('-p', '--pattern',
                         type=util.arg_type_non_empty2,
                         nargs='+',
                         default=None,
                         help='解密目录下文件时，默认会解密所有符合的文件，有此选项则只'
                              '解密文件名中含有指定内容的文件')
    decrypt.add_argument('-x', '--exclude_pattern',
                         action='store_true',
                         help='此选项需与pattern选项一起使用，解密目录下文件时，默认'
                              '会解密所有符合的文件，有此选项则只解密文件名中不含指定'
                              '内容的文件')

    # list子命令，用于列出目录下加密文件
    list_ = subparsers.add_parser(
        'list',
        help='列出目录下加密相关文件',
        parents=[util.arg_log()])
    list_.add_argument('dir',
                       type=util.arg_type_dir,
                       help='待列出加密相关文件的目录的完整路径')
    list_.add_argument(
        '-r', '--range',
        choices=[
            'encrypted',
            'decrypted',
            'both',
            'extension',
            'special',
            'empty'
        ],
        default='decrypted',
        help='指定列出范围，可用值有encrypted、decrypted、both、extension、special和'
             'empty，encrypted指加密文件，decrypted指加密文件的原文件，当加密文件存在'
             '时，如果剥离加密扩展名后的文件也存在且符合一定要求，这个剥离加密扩展名的文件'
             '就称作加密文件的原文件，both指即是加密文件又是原文件的文件，当加密文件被再次'
             '加密时会有这种文件，extension指具有加密扩展名的文件，special是指具有加密'
             '扩展名但不是加密文件的文件，empty指空文件，默认是decrypted'
    )
    list_.add_argument('-c', '--count',
                       action='store_true',
                       help='列出目录下加密相关文件时，有此选项则只统计数量')
    list_.add_argument('-e', '--expose',
                       action='store_true',
                       help='列出目录下加密相关文件时，默认会忽略目录下所有以“.”字符开'
                            '头的文件或者目录，有此选项则不忽略')

    # remove子命令，用于移除目录下加密相关文件
    remove = subparsers.add_parser(
        'remove',
        help='移除目录下加密相关文件',
        parents=[util.arg_log()])
    remove.add_argument('dir',
                        type=util.arg_type_dir,
                        help='待移除加密相关文件的目录的完整路径')
    remove.add_argument(
        '-r', '--range',
        choices=[
            'encrypted',
            'decrypted',
            'both',
            'extension'
        ],
        default='decrypted',
        help='指定移除范围，可用值有encrypted、decrypted、both、extension，'
             'encrypted指加密文件，decrypted指加密文件的原文件，当加密文件存在时，如果'
             '剥离加密扩展名后的文件也存在且符合一定要求，这个剥离加密扩展名的文件'
             '就称作加密文件的原文件，both指即是加密文件又是原文件的文件，当加密文件被再次'
             '加密时会有这种文件，extension指具有加密扩展名的文件，默认是decrypted'
    )
    remove.add_argument('-e', '--expose',
                        action='store_true',
                        help='移除目录下加密相关文件时，默认会忽略目录下所有以“.”字符开'
                             '头的文件或者目录，有此选项则不忽略')

    subcommands = {
        'encrypt': encrypt,
        'decrypt': decrypt,
        'list': list_,
        'remove': remove
    }

    global args
    args = parser.parse_args()
    args = check_args(args, subcommands[args.subcommand])

def log_config():
    if hasattr(args, 'log'):
        if hasattr(args, 'log_identifier') and args.log_identifier:
            return (
                '%s_v%s' % (__product__, __version__),
                args.log,
                args.log_identifier
            )
        else:
            return ('%s_v%s' % (__product__, __version__), args.log)

    return None

def init():
    log.info('program version: %s %s', __product__, __version__)
    log.debug('command line: %s', sys.argv)
    log.debug('parse args: %s', args)

    # 获取操作系统密码以便任务完成后自动关机
    if hasattr(args, 'shutdown'):
        global password
        password = util.get_os_password(args.shutdown)

    if hasattr(args, 'key'):
        global cipher_key
        cipher_key = get_key(args.key)

def schedule_tasks():
    if args.subcommand == 'encrypt':
        encrypt()
    elif args.subcommand == 'decrypt':
        decrypt()
    elif args.subcommand == 'list':
        list_()
    elif args.subcommand == 'remove':
        remove()

def clean():
    # 自动关机
    if hasattr(args, 'shutdown'):
        util.shutdown(password)
